package com.example.up;

import android.os.Environment;
import android.os.Handler;
import android.os.Looper;
import android.support.annotation.NonNull;
import android.util.Log;

import com.google.gson.Gson;
import com.google.gson.internal.$Gson$Types;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.net.FileNameMap;
import java.net.URLConnection;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;

import okhttp3.Cache;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.MediaType;
import okhttp3.MultipartBody;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okio.Buffer;
import okio.BufferedSink;
import okio.Okio;
import okio.Source;

/**
 * Created by yinghao on 2017/2/9.
 * Email：yhaowa@outlook.com
 */

public class OKHttp {

    private static OKHttp mInstance;
    private OkHttpClient mOkHttpClient;
    private Handler mMainHandler;
    private Gson mGson;

    private static final String TAG = "OKHttp";

    private OKHttp() {
        File sdCache = getExternalCacheDir();
        int cacheSize = 10 * 1024 * 1024;
        mOkHttpClient = new OkHttpClient.Builder()
                .connectTimeout(10, TimeUnit.SECONDS)
                .writeTimeout(20, TimeUnit.SECONDS)
                .readTimeout(20, TimeUnit.SECONDS)
                .cache(new Cache(sdCache.getAbsoluteFile(), cacheSize))
                .build();
        mMainHandler = new Handler(Looper.getMainLooper());
        mGson = new Gson();
    }

    private File getExternalCacheDir() {
        String path = Environment.getExternalStorageDirectory().getAbsolutePath() + "/" + "OkHttpCache";
        File sdCache = new File(path);
        if (!sdCache.exists()) {
            sdCache.mkdir();
        }
        return sdCache;
    }

    private static OKHttp getInstance() {
        if (mInstance == null) {
            synchronized (OKHttp.class) {
                if (mInstance == null) {
                    mInstance = new OKHttp();
                }
            }
        }
        return mInstance;
    }

    /**
     * 设置最大請求数
     */
    static void setMaxRequest(int n) {
        getInstance()._setMaxRequest(n);
    }

    private void _setMaxRequest(int n) {
        mOkHttpClient.dispatcher().setMaxRequests(n);
    }

    /**
     * 设置同一主机最大請求数
     */
    static void setMaxHostRequest(int n) {
        getInstance()._setMaxHostRequest(n);
    }

    private void _setMaxHostRequest(int n) {
        mOkHttpClient.dispatcher().setMaxRequestsPerHost(n);
    }

    /**
     * 取消指定tag的所有請求
     */
    static void cancel(String fileTag) {
        getInstance()._cancel(fileTag);
    }

    private void _cancel(String fileTag) {
        for (Call call : mOkHttpClient.dispatcher().queuedCalls()) {
            if (call.request().tag().equals(fileTag)) {
                call.cancel();
            }
        }
        for (Call call : mOkHttpClient.dispatcher().runningCalls()) {
            if (call.request().tag().equals(fileTag)) {
                call.cancel();
            }
        }
    }


    /**
     * 异步的post请求
     */
    public static void postAsyn(String url, final ResultCallback<String> callback, Map<String, String> params) {
        getInstance()._postAsyn(url, callback, params);
    }

    private void _postAsyn(String url, final ResultCallback<String> callback, Map<String, String> params) {
        OKHttp.Param[] paramsArr = map2Params(params);
        Request request = buildPostRequest(url, paramsArr);
        deliveryResult(callback, request, null);
    }


    /**
     * 异步基于post的文件上传，单文件且携带其他form参数上传
     */

    public static void postAsyn(String url, ResultCallback<String> callback, @NonNull ProgressListener progressListener, File file, String fileKey, Map<String, String> params, RequestInfoListener requestListener) {
        getInstance()._postAsyn(url, callback, progressListener, file, fileKey, params, requestListener);
    }

    public static void postAsyn(String url, ResultCallback<String> callback, File file, String fileKey, Map<String, String> params) {
        getInstance()._postAsyn(url, callback, null, file, fileKey, params, null);
    }

    private void _postAsyn(String url, ResultCallback<String> callback, ProgressListener progressListener, File file, String fileKey, Map<String, String> params, RequestInfoListener requestListener) {
        OKHttp.Param[] paramsArr = map2Params(params);
        Request request = buildMultipartFormRequest(url, progressListener, file, fileKey, params.get("fileTag"), paramsArr, requestListener);
        deliveryResult(callback, request, requestListener);
    }

    private Request buildMultipartFormRequest(String url, final ProgressListener progressListener, File file,
                                              String fileKey, String fileTag, OKHttp.Param[] params,
                                              RequestInfoListener requestListener
    ) {
        params = validateParam(params);
        MultipartBody.Builder builder = new MultipartBody.Builder().setType(MultipartBody.FORM);
        for (OKHttp.Param param : params) {
            builder.addFormDataPart(param.key, param.value);
        }
        String fileName = file.getName();
        RequestBody fileBody;
        if (progressListener == null) {
            fileBody = RequestBody.create(MediaType.parse(guessMimeType(fileName)), file);
        } else {
            fileBody = createOnProgressRequestBody(MediaType.parse(guessMimeType(fileName)), file, new ProgressListener() {
                @Override
                public void onProgress(long fileLength, long readBytesCount, boolean blockReadDone, long blockStartTime) {
                    sendProgressCallback(fileLength, readBytesCount,
                            blockReadDone, blockStartTime, progressListener);
                }
            }, requestListener);
        }
        builder.addFormDataPart(fileKey, fileName, fileBody);
        RequestBody requestBody = builder.build();
        return new Request.Builder()
                .url(url)
                .post(requestBody)
                .tag(fileTag)
                .build();
    }


    /**
     * 创建包含进度回调的RequestBody
     *
     * @param contentType
     * @param file
     * @param listener
     * @return
     */
    private RequestBody createOnProgressRequestBody(final MediaType contentType, final File file, final ProgressListener listener
            , final RequestInfoListener requestListener) {
        return new RequestBody() {
            @Override
            public MediaType contentType() {
                return contentType;
            }

            @Override
            public long contentLength() {
                return file.length();
            }

            @Override
            public void writeTo(BufferedSink sink) throws IOException {
                Source source;
                source = Okio.source(file);
                Buffer buf = new Buffer();
                sendRequestInfo(requestListener);
                long blockLength = file.length();
                long[] blockReadCount = new long[]{0};
                long blockStartTime = System.currentTimeMillis();
                for (long readCount; (readCount = source.read(buf, 2048)) != -1; ) {
                    sink.write(buf, readCount);
                    blockReadCount[0] += readCount;
                    if (blockReadCount[0] == blockLength) {
                        listener.onProgress(blockLength, blockLength, true, blockStartTime);
                    }
                }
            }
        };
    }

    //观察okhttp内部请求信息
    public interface RequestInfoListener {
        void onRequest(int maxRequest, int maxHostRequest, int runningRequest, int queuedRequest);
    }


    public interface ProgressListener {
        void onProgress(long fileLength, long readBytesCount, boolean blockReadDone, long blockStartTime);
    }


    private OKHttp.Param[] map2Params(Map<String, String> params) {
        if (params == null) return new OKHttp.Param[0];
        int size = params.size();
        OKHttp.Param[] res = new OKHttp.Param[size];
        Set<Map.Entry<String, String>> entries = params.entrySet();
        int i = 0;
        for (Map.Entry<String, String> entry : entries) {
            res[i++] = new OKHttp.Param(entry.getKey(), entry.getValue());
        }
        return res;
    }

    private Request buildPostRequest(String url, OKHttp.Param[] params) {
        if (params == null) {
            params = new OKHttp.Param[0];
        }
        MultipartBody.Builder builder = new MultipartBody.Builder().setType(MultipartBody.FORM);
        for (OKHttp.Param param : params) {
            builder.addFormDataPart(param.key, param.value);
        }
        RequestBody requestBody = builder.build();
        return new Request.Builder()
                .url(url)
                .post(requestBody)
                .build();
    }

    public static abstract class ResultCallback<T> {
        Type mType;

        public ResultCallback() {
            mType = getSuperclassTypeParameter(getClass());
        }

        static Type getSuperclassTypeParameter(Class<?> subclass) {
            Type superclass = subclass.getGenericSuperclass();
            if (superclass instanceof Class) {
                throw new RuntimeException("Missing type parameter.");
            }
            ParameterizedType parameterized = (ParameterizedType) superclass;
            return $Gson$Types.canonicalize(parameterized.getActualTypeArguments()[0]);
        }

        public abstract void onError(Request request, Exception e);

        public abstract void onResponse(T response);
    }

    private void deliveryResult(final ResultCallback<String> callback, Request request,
                                final RequestInfoListener requestListener) {
        enqueueCall(mOkHttpClient.newCall(request), callback, request, requestListener);
    }

    private void enqueueCall(Call call, final ResultCallback<String> callback, final Request request,
                             final RequestInfoListener requestListener) {

        call.clone().enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                sendRequestInfo(requestListener);
                Log.e(TAG, "onFailure: " + e.getMessage());
                //TODO 失败重传策略
                Log.e(TAG, "enqueueCall: ");
                try {
                    Thread.sleep(50);
                } catch (InterruptedException e1) {
                    e1.printStackTrace();
                }
                enqueueCall(call, callback, request, requestListener);
                sendFailedStringCallback(call, e, callback);



            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {

                try {
                    final String string = response.body().string();
                    if (callback.mType == String.class) {
                        sendSuccessResultCallback(string, callback);
                        deleteTempFile(string);
                    } else {
                        Object o = mGson.fromJson(string, callback.mType);
                        sendSuccessResultCallback(o, callback);
                    }

                } catch (IOException | com.google.gson.JsonParseException e) {
                    sendFailedStringCallback(null, e, callback);
                }
                sendRequestInfo(requestListener);
            }
        });
    }

    private void sendRequestInfo(final RequestInfoListener listener) {
        mMainHandler.post(new Runnable() {
            @Override
            public void run() {
                if (listener != null) {
                    listener.onRequest(mOkHttpClient.dispatcher().getMaxRequests(),
                            mOkHttpClient.dispatcher().getMaxRequestsPerHost(),
                            mOkHttpClient.dispatcher().runningCallsCount(),
                            mOkHttpClient.dispatcher().queuedCallsCount());
                }
            }
        });
    }

    private void deleteTempFile(String fileName) {
        //  在okhttp请求线程中删除临时文件
        //TODO： 请求线程崩溃时会有若干临时文件垃圾
        File file = new File(FileAccess.TEMP_PATH, fileName);
        if (file.exists()) {
            file.delete();
        }

    }

    private void sendFailedStringCallback(final Call call, final Exception e, final ResultCallback<String> callback) {
        mMainHandler.post(new Runnable() {
            @Override
            public void run() {
                if (callback != null)
                    callback.onError(null, e);
            }
        });
    }

    private void sendSuccessResultCallback(final Object object, final OKHttp.ResultCallback callback) {
        mMainHandler.post(new Runnable() {
            @Override
            public void run() {
                if (callback != null) {
                    callback.onResponse(object);
                }
            }
        });
    }

    private void sendProgressCallback(final long fileLength, final long readBytesCount, final boolean blockReadDone, final long blockStartTime, final ProgressListener progressListener) {
        mMainHandler.post(new Runnable() {
            @Override
            public void run() {
                if (progressListener != null)
                    progressListener.onProgress(fileLength, readBytesCount, blockReadDone, blockStartTime);
            }
        });
    }

    private String guessMimeType(String path) {
        FileNameMap fileNameMap = URLConnection.getFileNameMap();
        String contentTypeFor = fileNameMap.getContentTypeFor(path);
        if (contentTypeFor == null) {
            contentTypeFor = "application/octet-stream";
        }
        return contentTypeFor;
    }


    private OKHttp.Param[] validateParam(OKHttp.Param[] params) {
        return params == null ? new OKHttp.Param[0] : params;
    }

    private static class Param {
        String key;
        String value;

        private Param(String key, String value) {
            this.key = key;
            this.value = value;
        }
    }


}
